iT邦幫忙

2021 iThome 鐵人賽

DAY 25
0
Mobile Development

認真學 Compose - 對 Jetpack Compose 的問題與探索系列 第 25

D25 / 為什麼 State 改變會觸發 recomposition - State & Snapshot system

  • 分享至 

  • xImage
  •  

今天大概會聊到的範圍

  • Snapshot system

上一篇有提到,State 改變時會觸發 recomposition。視這個行為是一種定義。但是為什麼?這怎麼發生的呢?

今天要介紹的是 Compose 中另一個與 State 息息相關的概念 - Snapshot

什麼是 Snapshot

Snapshot 可以想像是當下所有 State 的存檔、快照。就像遊戲存檔或是電腦備份一樣,當下是什麼就是什麼,全部存起來。

提醒:Snapshot 通常不會用在 Compose 的開發中,主要是在用於 Compose 的內部運作。

讓我們看看 Snapshot 實際上怎麼使用的:

fun main() {

    // 1. 
    val data = mutableStateOf("") 
    
    // 2.
    data.value = "Foo"
    println("2: ${data.value}")
    
    // 3.
    val snap = Snapshot.takeSnapshot()
    
    // 4.
    data.value = "Bar"
    println("4: ${data.value}")
    
    // 5. 
    snap.enter {
        println("5: ${data.value}")
    }

    // 6.
    println("6: ${data.value}")
    
    
    // 7.
    snap.dispose()
}

Snapshot 和 State 的運作,是不一定要在 composable function 中執行的

Output:

2: Foo
4: Bar
5: Foo
6: Bar
  1. 透過 mutableStateOf 建立 State。
  2. 編輯 State 的 value 後,印出來的也不意外是 "Foo"
  3. 在這時我們拍一張 sanpshot,將當下的 State 通通存起來
  4. 再次編輯 State 的 value,並可以看到 State 的確有被我們修改
  5. 透過 Snapshot.enter 可以 access 當時拍的 snapshot,在這個 scope 內,我們可以取得當時的 state
  6. 在 snapshot 外,state 依然是編輯後的 Bar,並不會因為我們進去 snap 就改變
  7. 使用完 snapshot 機制後,要將其 dispose 釋放掉資源

Snapshot 的概念就是這麼單純:保留某一個時刻的所有 State。

MutableSnapshot

在 enter snapshot 後,如果我們還想對 State 進行編輯,將會得到錯誤。

    snap.enter {
        data.value = "Buzz"  // << --- Error
    }

在 snapshot 中,State 是唯讀無法編輯的。若希望編輯,我們會需要 MutableSnapshot

fun main() {
    val data = mutableStateOf("")
    
    data.value = "Foo"
    
    // 1.
    val mutableSnap = Snapshot.takeMutableSnapshot()
    
    // 2.
    mutableSnap.enter {
        println("2: ${data.value}")
        data.value = "Buzz"
        println("3: ${data.value}")
    }
    
    
    println("4: ${data.value}")
    
    // 5.
    mutableSnap.enter {
        println("5: ${data.value}")
    }
    
    // 6.
    mutableSnap.apply()
    println("6: ${data.value}")
    
    mutableSnap.dispose()
}
Output:

2: Foo
3: Buzz
4: Foo
5: Buzz
6: Buzz
  1. 我們使用 takeMutableSnapshot 來建立 MutableSnapshot
  2. MutableSnapshot 中,我們一樣可以拿到 takeSnapshot 時的資料 "Foo"
  3. MutableSnapshot 中,我們可以對 Sate 進行編輯
  4. 但這個編輯不會影響到外部的 State
  5. 再次進入 snapshot,可以再次拿到曾經在 snapshot 中編輯的 State,就樣平行時空一樣。
  6. 透過 MutableSnapshot.apply() 我們可以將 snapshot 的值實際賦予到 State 身上
  7. 注意,如果再 takeMutableSnapshotapply 之間,有人直接對 State set value,apply 也不會發生作用

監聽 takeMutableSnapshot

fun takeMutableSnapshot(
    readObserver: ((Any) -> Unit)? = null,
    writeObserver: ((Any) -> Unit)? = null
): MutableSnapshot =
    (currentSnapshot() as? MutableSnapshot)?.takeNestedMutableSnapshot(
        readObserver,
        writeObserver
    ) ?: error("Cannot create a mutable snapshot of an read-only snapshot")

其實仔細看看 takeMutableSnapshot 的 signature,會發現其實他還有吃兩個參數:read/write 的 Observer。
實際上我們可以透過這兩個角色去監聽到資料被寫入、被讀取的時機,並且取得即將被異動的 State 與其資料。

fun main() {
    val data = mutableStateOf("")
    
    data.value = "Foo"
    
    val readObserver = { readState: Any -> if (readState == data) println("READ")}
    
    val writeObserver = { readState: Any -> if (readState == data) println("WRITE")}
    
    val mutableSnap = Snapshot.takeMutableSnapshot(readObserver, writeObserver)
    
    mutableSnap.enter {
        println("1: ${data.value}")
        data.value = "Buzz"
        println("2: ${data.value}")
    }
    
    println("3: ${data.value}")
    
    mutableSnap.apply()
    println("4: ${data.value}")
    
    mutableSnap.dispose()
}
READ
1: Foo
WRITE
READ
2: Buzz
3: Foo
4: Buzz

println 前,我們需要去讀取 data.value 並將資料喂給 println function ,因此每次在 snapshot 內的 println 之前都會觸發 READ

回頭說說 Recomposition

Composable 最後會被丟到 Recomposer 去執行,在 Recomposer 中我們可以發現這段 code

private inline fun <T> composing(
    composition: ControlledComposition,
    modifiedValues: IdentityArraySet<Any>?,
    block: () -> T
): T {
    val snapshot = Snapshot.takeMutableSnapshot(
        readObserverOf(composition), writeObserverOf(composition, modifiedValues)
    )
    try {
        return snapshot.enter(block)
    } finally {
        applyAndCheck(snapshot)
    }
}

private fun readObserverOf(composition: ControlledComposition): (Any) -> Unit {
    return { value -> composition.recordReadOf(value) }
}


// 

override fun recordReadOf(value: Any) {
    if (!areChildrenComposing) {
        composer.currentRecomposeScope?.let {    // <--- 
            it.used = true
            observations.add(value, it)
            
            ... 
        }
    }
}

source

Recomposer 在 composing 時,利用 snapshot 的 read/write observer 關注在 snapshot 中的 state 變化。並且在 read observer 時建立 recompose scope。

private fun writeObserverOf(
    composition: ControlledComposition,
    modifiedValues: IdentityArraySet<Any>?
): (Any) -> Unit {
    return { value ->
        composition.recordWriteOf(value)
        modifiedValues?.add(value)
    }
}


override fun recordWriteOf(value: Any) = synchronized(lock) {
    // ...
    derivedStates.forEachScopeOf(value) { // <--- 
        invalidateScopeOfLocked(it)
    }
}

在資料變動時,會 iterate 各個 scope 並且將他們 invalidate。


今天聊到的東西非常的底層,因為篇幅的關係,有些邏輯沒有說明到。非常推薦大家到下面 Reference 的地方閱讀本篇主要參考的文章。看了這些之後感覺對整個 compose 運作流程又再更近一步的認識了!


Reference:


上一篇
D24 / 什麼時候我的 Composable function 會重新被呼叫 - recompose
下一篇
D26 / 要怎麼放 Compose 進我的專案? - Compose View
系列文
認真學 Compose - 對 Jetpack Compose 的問題與探索30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言